/******************************************************************************
	Cobalt - utility libraries for building XNA games
	Copyright (C) 2008 Philip Djonov

	This program is free software; you can redistribute it and/or modify it
	under the terms of the GNU General Public License as published by the Free
	Software Foundation; either version 2 of the License, or (at your option)
	any later version.

	This program is distributed in the hope that it will be useful, but
	WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
	or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
	for more details.

	You should have received a copy of the GNU General Public License along
	with this program; if not, write to the
	
		Free Software Foundation, Inc.
		51 Franklin Street, Fifth Floor
		Boston, MA  02110-1301, USA.
******************************************************************************/

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

using Cobalt.Framework;
using Cobalt.Framework.Bsp;
using Cobalt.Framework.Console;
using Cobalt.Framework.Graphics;
using Cobalt.Framework.UI;

using Console = Cobalt.Framework.Console.Console;

namespace Cobalt
{

	public class SampleGame : Microsoft.Xna.Framework.Game
	{
		private GraphicsDeviceManager graphics;

		public SampleGame()
		{
			graphics = new GraphicsDeviceManager( this );
			graphics.PreferMultiSampling = false;

			graphics.MinimumVertexShaderProfile = ShaderProfile.VS_2_0;
			graphics.MinimumPixelShaderProfile = ShaderProfile.PS_2_0;

			graphics.PreparingDeviceSettings += delegate( object sender, PreparingDeviceSettingsEventArgs e )
			{
				e.GraphicsDeviceInformation.PresentationParameters.EnableAutoDepthStencil = false;
				e.GraphicsDeviceInformation.PresentationParameters.BackBufferCount = 1;
				e.GraphicsDeviceInformation.PresentationParameters.RenderTargetUsage = RenderTargetUsage.PlatformContents;
			};

			Content.RootDirectory = "Content";
			
#if DEBUG
			IsFixedTimeStep = false;
			graphics.SynchronizeWithVerticalRetrace = false;
#endif
		}

		private FrameStats frameStats;
		private PostProcess compositer;
		private MaterialManager materials;
		
		private Camera camera;

		private Console console;
		private UiContext uiCtx;
		private ConsoleVariablesUI consoleUi;

		protected override void Initialize()
		{
			uiCtx = new UiContext( this );
			Components.Add( uiCtx );

			uiCtx.RegisterInputProvider( new GamePadInput( PlayerIndex.One ) );
			uiCtx.BeforeRoute += new UiContext.RouteEventHandler( uiCtx_BeforeRoute );

			frameStats = new FrameStats( this );
			Components.Add( frameStats );

			compositer = new PostProcess( this );
			Components.Add( compositer );

			compositer.Context.PreferMultiSampling = true;

			SetupConsole();

			//set up the content services

			materials = new MaterialManager( Services, Content );
			materials.RootDirectory = "Materials";
			Services.AddService( typeof( IMaterialService ), materials );

			camera = new Camera();

			base.Initialize();
		}

		private void SetupConsole()
		{
			console = new Console();

			Variable var;
			var intMetaBuilder = new VariableMetadata<int>.Builder();
			var floatMetaBuilder = new VariableMetadata<float>.Builder();
			var vec3MetaBuilder = new VariableMetadata<Vector3>.Builder();

			const string bloomHeading = "PostProc/Bloom";
			const string dofHeading = "PostProc/DOF";

			//set up the bloom vars

			var = new Variable<bool>( "Enable", VariableMetadata<bool>.CreateSimple( true ) );
			var.ValueChanged += ( s, e ) => compositer.Context.BloomEnable = (s as Variable<bool>).Value;
			console.RegisterVariable( var, bloomHeading );

			intMetaBuilder.DefaultValue = 3;
			intMetaBuilder.ChangeValidator = ( o, n ) => n > 0 && n <= 25;

			var = new Variable<int>( "Blur Steps", intMetaBuilder.CreateMetadata() );
			var.ValueChanged += ( s, e ) => compositer.Context.BlurSteps = (s as Variable<int>).Value;
			console.RegisterVariable( var, bloomHeading );

			floatMetaBuilder.DefaultValue = 5;
			floatMetaBuilder.ChangeValidator = ( o, n ) => n >= 0 && n < 25;

			var = new Variable<float>( "Blur Strength", floatMetaBuilder.CreateMetadata() );
			var.ValueChanged += ( s, e ) => compositer.Context.BlurStrength = (s as Variable<float>).Value;
			console.RegisterVariable( var, bloomHeading );

			floatMetaBuilder.DefaultValue = 0.125F;
			floatMetaBuilder.ChangeValidator = ( o, n ) => n >= 0 && n < 1;

			var = new Variable<float>( "Threshhold", floatMetaBuilder.CreateMetadata() );
			var.ValueChanged += ( s, e ) => compositer.Context.BloomThresh = (s as Variable<float>).Value;
			console.RegisterVariable( var, bloomHeading );

			floatMetaBuilder.DefaultValue = 1.25F;
			floatMetaBuilder.ChangeValidator = ( o, n ) => n >= -10 && n <= 10;

			var = new Variable<float>( "Bloom Saturation", floatMetaBuilder.CreateMetadata() );
			var.ValueChanged += ( s, e ) => compositer.Context.BloomSaturation = (s as Variable<float>).Value;
			console.RegisterVariable( var, bloomHeading );

			floatMetaBuilder.DefaultValue = 1.0F;

			var = new Variable<float>( "Base Saturation", floatMetaBuilder.CreateMetadata() );
			var.ValueChanged += ( s, e ) => compositer.Context.BaseSaturation = (s as Variable<float>).Value;
			console.RegisterVariable( var, bloomHeading );

			vec3MetaBuilder.DefaultValue = new Vector3( 1.25F );
			vec3MetaBuilder.ChangeValidator = ( o, n ) =>
				n.X >= -10 && n.X <= 10 &&
				n.Y >= -10 && n.Y <= 10 &&
				n.Z >= -10 && n.Z <= 10;

			var = new Variable<Vector3>( "Base Tint", vec3MetaBuilder.CreateMetadata() );
			var.ValueChanged += ( s, e ) => compositer.Context.BaseTint = (s as Variable<Vector3>).Value;
			console.RegisterVariable( var, bloomHeading );

			vec3MetaBuilder.DefaultValue = new Vector3( 1.0F );

			var = new Variable<Vector3>( "Bloom Tint", vec3MetaBuilder.CreateMetadata() );
			var.ValueChanged += ( s, e ) => compositer.Context.BloomTint = (s as Variable<Vector3>).Value;
			console.RegisterVariable( var, bloomHeading );

			//set up the DOF values

			var = new Variable<bool>( "Enable", VariableMetadata<bool>.CreateSimple( false ) );
			var.ValueChanged += ( s, e ) => compositer.Context.DofEnable = (s as Variable<bool>).Value;
			console.RegisterVariable( var, dofHeading );

			floatMetaBuilder.DefaultValue = 1.0F;
			floatMetaBuilder.ChangeValidator = ( o, n ) => n >= 0;

			var = new Variable<float>( "Near Focus", floatMetaBuilder.CreateMetadata() );
			var.ValueChanged += ( s, e ) => compositer.Context.DofNearFocus = (s as Variable<float>).Value;
			console.RegisterVariable( var, dofHeading );

			floatMetaBuilder.DefaultValue = 0;

			var = new Variable<float>( "Near Distance", floatMetaBuilder.CreateMetadata() );
			var.ValueChanged += ( s, e ) => compositer.Context.DofNearDistance = (s as Variable<float>).Value;
			console.RegisterVariable( var, dofHeading );

			floatMetaBuilder.DefaultValue = 10;

			var = new Variable<float>( "Focal Distance", floatMetaBuilder.CreateMetadata() );
			var.ValueChanged += ( s, e ) => compositer.Context.DofFocalDistance = (s as Variable<float>).Value;
			console.RegisterVariable( var, dofHeading );

			floatMetaBuilder.DefaultValue = 20;

			var = new Variable<float>( "Far Distance", floatMetaBuilder.CreateMetadata() );
			var.ValueChanged += ( s, e ) => compositer.Context.DofFarDistance = (s as Variable<float>).Value;
			console.RegisterVariable( var, dofHeading );

			console.FinishRegistration();
		}

		private void uiCtx_BeforeRoute( object sender, UiContext.RouteEventArgs e )
		{
			var vbe = e.Event as VirtualButtonEvent;
			if( vbe != null )
			{
				if( vbe.Button == VirtualButton.Start )
				{
					if( uiCtx.IsVisible( consoleUi ) )
					{
						uiCtx.Hide( consoleUi );
					}
					else
					{
						uiCtx.Show( consoleUi );
					}
				}
			}
		}

		private Matrix levelMatrix, inverseLevelMatrix;
		private ISurfaceList levelGeometry;
		private IPvs levelPvs;

		private BspData.Trace levelTrace;

		private DrawList drawList;
		private ShadingContext shadeCtx;

		private Vector4? spawnParams;

		protected override void LoadContent()
		{
#if WINDOWS
			PresentationParameters presParams = GraphicsDevice.PresentationParameters;
			screenShotTarget = new ResolveTexture2D( GraphicsDevice, presParams.BackBufferWidth,
				presParams.BackBufferHeight, 1, SurfaceFormat.Color );
#endif

			Rectangle safeRect = GraphicsDevice.Viewport.TitleSafeArea;

			consoleUi = new ConsoleVariablesUI( console, uiCtx );
			consoleUi.Origin.SetSimpleValue( new Vector2( (safeRect.Width - 400) / 2, (safeRect.Height - 320) / 2 ) );

			shadeCtx = new ShadingContext( GraphicsDevice );
			drawList = new DrawList( this.Services );

			//levelGeometry = Content.Load<ISurfaceList>( @"omg\maps\apts0" );
			//levelGeometry = Content.Load<ISurfaceList>( @"cobalt\maps\map00" );
			levelGeometry = Content.Load<ISurfaceList>( @"chiroptera\maps\chiropteradm" );
			//levelGeometry = Content.Load<ISurfaceList>( @"mg_final\maps\mg_final" );
			//levelGeometry = Content.Load<ISurfaceList>( @"twq3tourney2\maps\twq3tourney2" );

			levelPvs = levelGeometry.CreatePvs();

			BspData bsp = levelGeometry as BspData;
			if( bsp != null )
			{
				foreach( Entity ent in bsp.Entities.All )
				{
					if( ent.ClassName.StartsWith( "info_player" ) )
					{
						spawnParams = new Vector4( ent.Origin, ent.Angle );
						break;
					}
				}

				levelTrace = bsp.CreateTrace();
			}

			levelMatrix = Matrix.CreateScale( 1.0F / 64.0F );
			Matrix.Invert( ref levelMatrix, out inverseLevelMatrix );
		}

		protected override void UnloadContent()
		{
			consoleUi = null;

#if WINDOWS
			Helpers.DisposeAndNull( ref screenShotTarget );
#endif
		}

#if WINDOWS
		private bool doScreenShot;
		private ResolveTexture2D screenShotTarget;

		private void TakeScreenShot()
		{
			string screenName = string.Format( "{0:yyyy-MM-dd HH-mm-ss}.png", DateTime.Now );

			string picsDir = System.Environment.GetFolderPath( Environment.SpecialFolder.MyPictures );
			string screensDir = System.IO.Path.Combine( picsDir, "Cobalt" );
			string path = System.IO.Path.Combine( screensDir, screenName );

			System.IO.Directory.CreateDirectory( screensDir );

			GraphicsDevice.ResolveBackBuffer( screenShotTarget );
			screenShotTarget.Save( path, ImageFileFormat.Png );
		}
#endif

		public void MoveCamera( Vector2 move, Vector2 rotation, GameTime gameTime )
		{
			float time = (float)gameTime.ElapsedGameTime.TotalSeconds;

			if( rotation != Vector2.Zero )
			{
				Vector2 rv = Helpers.SmoothStep( Vector2.Zero, new Vector2( camera.RotationSpeed ), rotation * 0.25F ) * 4.0F;

				camera.Yaw -= rv.X * time * 1.5F;
				camera.Pitch -= rv.Y * time;
			}

			if( move.LengthSquared() > 0.01F )
			{
				Vector3 moveDir = new Vector3( move.X, 0, -move.Y );
				moveDir = Vector3.Transform( moveDir, camera.Oreintation );

				Vector3 moveRay = moveDir * time * camera.MovementSpeed;

				const float TraceEps = 0.01F;

				Vector3 traceStart = camera.Position;
				Vector3 traceEnd = camera.Position + moveRay;

				Vector3.Transform( ref traceStart, ref inverseLevelMatrix, out traceStart );
				Vector3.Transform( ref traceEnd, ref inverseLevelMatrix, out traceEnd );

				moveRay = traceEnd - traceStart;

				Vector3 initialPos = traceStart;
				float initialMoveDist = moveRay.Length();

				if( initialMoveDist <= TraceEps )
					return;

				float sx = inverseLevelMatrix.Left.Length();
				float sy = inverseLevelMatrix.Up.Length();
				float sz = inverseLevelMatrix.Forward.Length();

				float maxScale = Math.Max( sx, Math.Max( sy, sz ) );

				BrushContents brushMask = BrushContents.Solid | BrushContents.PlayerClip;

				levelTrace.ContentMask = brushMask;

				BoundingSphere sphere = camera.ApertureBounds;
				sphere.Radius *= maxScale;

				levelTrace.TraceBox = BoundingBox.CreateFromSphere( sphere );
				levelTrace.Type = BspData.Trace.TraceType.Box;

				for( int steps = 0; levelTrace.TraceRay( traceStart, traceEnd ); steps++ )
				{
					if( steps == 10 || moveRay.LengthSquared() <= TraceEps * TraceEps )
						return;

					if( levelTrace.HitDistance <= TraceEps )
					{
						//move us a tiny bit "out"
						traceStart += levelTrace.HitPlane.Normal * Helpers.Saturate( TraceEps - levelTrace.HitDistance );

						//we hit right away, blow out *everything* in common with the plane normal
						SubtractOncomingComponent( moveRay, levelTrace.HitPlane.Normal, out moveRay );

						traceEnd = traceStart + moveRay;

						continue;
					}

					//move us up to where we hit
					Vector3 v = moveRay * Helpers.Saturate( levelTrace.HitFraction - TraceEps );
					traceStart += v;

					//get the remaining motion
					Vector3 r = moveRay - v;
					//reduce it to the component parallel to the hit plane
					SubtractOncomingComponent( r, levelTrace.HitPlane.Normal, out moveRay );

					//and set up for the next trace
					traceEnd = traceStart + moveRay;
				}

				if( moveRay.LengthSquared() <= TraceEps * TraceEps )
					return;

				Vector3 newPos = traceStart + moveRay;

				float moveDist;
				Vector3.Distance( ref initialPos, ref newPos, out moveDist );

				if( moveDist > initialMoveDist )
					return;

				BspData bsp = levelGeometry as BspData;
				if( bsp != null )
				{
					BrushContents contents = bsp.GetPointContents( newPos ).GetValueOrDefault( BrushContents.None );
					if( (contents & brushMask) != BrushContents.None )
						return;
				}

				camera.Position = Vector3.Transform( newPos, levelMatrix );
			}
		}

		private static void SubtractOncomingComponent( Vector3 vA, Vector3 vN, out Vector3 vO )
		{
			float dot;
			Vector3.Dot( ref vA, ref vN, out dot );

			if( dot >= 0 )
			{
				vO = vA;
				return;
			}
			else
			{
				vO = vA - vN * dot;
			}
		}

		private void PrepForRender( GameTime gameTime )
		{
			shadeCtx.View = camera.View;
			shadeCtx.Projection = camera.Projection;
			shadeCtx.RenderTime = (float)gameTime.TotalRealTime.TotalSeconds;

			if( levelPvs != null )
			{
				Vector3 levelPos = Vector3.Transform( camera.Position, inverseLevelMatrix );
				levelPvs.Mark( levelPos );
			}

			drawList.Reset();

			if( levelGeometry != null )
				levelGeometry.AppendSurfaces( drawList, levelPvs, levelMatrix, shadeCtx );

			drawList.Sort();

			drawList.Prerender( shadeCtx );
		}

		private GamePadState oldPad;

		protected override void BeginRun()
		{
			oldPad = GamePad.GetState( PlayerIndex.One );

			if( spawnParams.HasValue )
			{
				Vector3 spawnOr = new Vector3( spawnParams.Value.X, spawnParams.Value.Y, spawnParams.Value.Z );
				float spawnAngle = spawnParams.Value.W;

				camera.Position = Vector3.Transform( spawnOr, levelMatrix );
				camera.Yaw = MathHelper.ToDegrees( -spawnAngle + MathHelper.PiOver2 );

				spawnParams = null;
			}
		}

		protected override void Update( GameTime gameTime )
		{
			base.Update( gameTime );

			GamePadState inPad = GamePad.GetState( PlayerIndex.One );

			// Allows the game to exit
			if( inPad.Buttons.Back == ButtonState.Pressed )
				this.Exit();

			if( !uiCtx.IsVisible( consoleUi ) )
			{
				//process game input

				if( inPad.PacketNumber != oldPad.PacketNumber )
				{
					if( inPad.Buttons.X == ButtonState.Pressed && oldPad.Buttons.X == ButtonState.Released )
					{
						shadeCtx.ShadingComplexityRank = (shadeCtx.ShadingComplexityRank + 1) % 3;
					}

					if( inPad.Buttons.Y == ButtonState.Pressed && oldPad.Buttons.Y == ButtonState.Released )
					{
						if( shadeCtx.OverrideMaterial == null )
							shadeCtx.OverrideMaterial = materials.GetMaterial( @"Debug\Wire" );
						else
							shadeCtx.OverrideMaterial = null;
					}

#if WINDOWS
					if( inPad.Buttons.RightShoulder == ButtonState.Pressed && oldPad.Buttons.RightShoulder == ButtonState.Released )
					{
						doScreenShot = true;
					}
#endif

					oldPad = inPad;
				}

				MoveCamera( inPad.ThumbSticks.Left, inPad.ThumbSticks.Right, gameTime );
			}

			PrepForRender( gameTime );
		}

		protected override void Draw( GameTime gameTime )
		{
			PerformanceCounters counters = shadeCtx.PerformanceCounters;
			counters.Reset();

			compositer.Render( drawList, shadeCtx, null );

#if WINDOWS
			if( doScreenShot )
			{
				TakeScreenShot();
				doScreenShot = false;
			}
#endif

			if( counters != null )
			{
				//stats
				TextBuffer buf = frameStats.CustomText;

				buf.Clear();
				buf.AppendText( "Counters:\n  " );
				buf.AppendInt( counters.SurfacesDrawn, 6, ' ' );
				buf.AppendText( " Surfaces\n  " );
				buf.AppendInt( counters.VerticesDrawn, 6, ' ' );
				buf.AppendText( " Vertices\n  " );
				buf.AppendInt( counters.PrimitivesDrawn, 6, ' ' );
				buf.AppendText( " Primitives\n  " );
				buf.AppendInt( counters.DrawPrimitivesCount, 6, ' ' );
				buf.AppendText( " Draw Calls\n  " );
			}

			base.Draw( gameTime );
		}
	}
}
